## Mobwrite Lib Python

import urllib
import diff_match_patch as dmp_module
import logging

class SharedData(object):
  clientText = ''
  deltaOk = False
  editStack = []
  shadow = ''
  clientVersion = 0
  serverVersion = 0
  
  def __init__(self):
    self.editStack = [] ## for some reason if not set here its shared across all instances
  
## the sync server
class MobwriteClient():
  def __init__(self, url, user):
    ## server url
    self.URL = url
    self.username = user
    self.in_sync = False
    self.dmp = dmp_module.diff_match_patch() ## Create a patch module object.
    self.shares = {} ## e.g. self.shares[fileId] = SharedData()
    self.callbacks = {} ## e.g. self.callbacks[fileId] = {'getText': func, 'applyPatch': func, 'applyRaw': func}
    
  def shareAdd(self, fileId, getText, applyPatch = None, applyRaw = None):
    ## does this already exist?
    if fileId in self.shares.keys():
      return -1
    obj = SharedData()
    self.shares[fileId] = obj
    
    ## check callbacks
    if getText == None:
      ## sync itself..
      getText = lambda: self.shares[fileId].clientText
    if applyPatch == None:
      ## patch itself? or return ''
      applyPatch = lambda p,d: self.dmp.apply_patch(p,d)
    if applyRaw == None:
      ## set itself? or return ''
      def ar(x):
        self.shares[fileId].clientText = x
      applyRaw = ar
    
    self.callbacks[fileId] = {'getText': getText, 'applyPatch': applyPatch, 'applyRaw': applyRaw}
    return 0

  def shareRemove(self, fileId):
    print "remove share"
    del self.shares[fileId]
    del self.callbacks[fileId]
    print len(self.shares)

  def updateClientTexts(self):
    # OK lets make sure everything from the user input is up-to-date
    # and add it to a list to send to sync process.
    for fileId in self.shares.keys():
      self.shares[fileId].clientText = self.callbacks[fileId]['getText']()
    
  def build_sync(self, syncdocs):
    dmp_sync = dmp_module.diff_match_patch()

    ## add changes to our editStacks ready to transmit
    for fileId in syncdocs.keys():
      syncdoc = syncdocs[fileId]
      if syncdoc.deltaOk:
        diff = dmp_sync.diff_main(syncdoc.shadow, syncdoc.clientText, True)    
    
        if len(diff) > 2:
          dmp_sync.diff_cleanupSemantic(diff)
          dmp_sync.diff_cleanupEfficiency(diff)

        ## Apply our changes :)
        syncdoc.shadow = syncdoc.clientText
    
        ## I dont handle mergeChanges
        action = 'd:' + str(syncdoc.clientVersion) + ':' + dmp_sync.diff_toDelta(diff)
      
        ##if (diffs.length != 1 || diffs[0][0] != DIFF_EQUAL) { mobwrite.syncChange_ = true; }      
        syncdoc.editStack.append([syncdoc.clientVersion, action])
        syncdoc.clientVersion += 1
      else:
        if syncdoc.shadow != syncdoc.clientText:
          syncdoc.shadow = syncdoc.clientText
        syncdoc.clientVersion += 1
        action = 'r:' + str(syncdoc.clientVersion) + ':' + syncdoc.clientText
        syncdoc.editStack.append([syncdoc.clientVersion, action])
    return syncdocs

  def sync(self, syncdocs):
    logging.debug("In Sync (thread 2)")
    ## Create the output starting with the file statement, followed by the edits.    
    
    xdata = ['u:' + self.username + '\n']
    
    for fileId in syncdocs.keys():
      syncdoc = syncdocs[fileId]
      data = 'F:' + str(syncdoc.serverVersion) + ':' + fileId + '\n'
      for x in range(0, len(syncdoc.editStack)):
        data = data + syncdoc.editStack[x][1] + '\n'

      xdata.append(data)
      xdata.append('\n')

    ## --- End of prep..
    xdata = ''.join(xdata)
    
    ## Send our changes to server.
    params = urllib.urlencode({'q': xdata})
    logging.debug("Sending Server %s", xdata.replace('\n','\\n'))
    
    try:
      f = urllib.urlopen(self.URL, params)
      res = f.read()
    except IOError,e:
      logging.error("IOError %s" , e)
      ## TODO read up on python try..except..
      return [-2, None]
    f.close()

    return [0, res]
  
  def syncResult(self, res):
    filen = None
    clientVersion = None

		## Apply the server changes.
    for line in res.splitlines(True):
      if not (line.endswith("\n") or line.endswith("\r")):
        # Truncated line.  Abort.
        logging.warning("Truncated line: '%s'" % line)
        break
      line = line.rstrip("\r\n")
      if not line:
        # Terminate on blank line.
        break
      if line.find(":") != 1:
        # Invalid line.
        continue
      (name, value) = (line[:1], line[2:])

      version = None
      
      if name == "F" or name == "f" or name == "d" or name == "R":
        div = value.find(":")
        version = int(value[:div])
        value = value[div+1:]
        
      if name == "F" or name == "f":
        filen = value
        self.shares[filen].deltaOk = True
        clientVersion = version
        
        self.shares[filen].editStack = filter((lambda x: x[0] > version), self.shares[filen].editStack)
      #else:
      #  filen = None
              
      elif name == "R" or name == "r":
        if filen != None:
          self.shares[filen].shadow = urllib.unquote(value)
          self.shares[filen].clientVersion = clientVersion
          self.shares[filen].serverVersion = version
          self.shares[filen].editStack = []
          if name == "R":
            ## apply to our buffer
            self.callbacks[filen]['applyRaw'](self.shares[filen].shadow)
            self.shares[filen].clientText = self.shares[filen].shadow
          
      elif name == "d":
        if filen != None:
          if clientVersion != self.shares[filen].clientVersion:
            self.shares[filen].deltaOk = False
          elif version > self.shares[filen].serverVersion:
            self.shares[filen].deltaOk = False
          elif version < self.shares[filen].serverVersion:
            logging.warning("Server version in past.")
          else:
            try:
              diffs = self.dmp.diff_fromDelta(self.shares[filen].shadow, value)
              self.shares[filen].serverVersion = self.shares[filen].serverVersion + 1
            except:
              diffs = []
              self.shares[filen].deltaOk = False
              logging.error("Delta mismatch - Panic")
              ### TODO diffs = [] is wrong..
              ## Should we not run another sync straight away?
            if diffs and len(diffs) > 0: ## or diffs[0][0] != self.dmp.DIFF_EQUAL:
              if name == "D":
                ## Overwrite text.
                self.shares[filen].shadow = self.dmp.diff_text2(diffs)
                self.callbacks[filen]['applyRaw'](self.shares[filen].shadow)
                self.shares[filen].clientText = self.shares[filen].shadow
              else:
                patches = self.dmp.patch_make(self.shares[filen].shadow, '', diffs)
                ## TODO make user defined patch_apply return mastertext,results
                self.callbacks[filen]['applyPatch'](patches, self.shares[filen].shadow)
                (mastertext, results) = self.dmp.patch_apply(patches, self.shares[filen].shadow)
                self.shares[filen].shadow = mastertext
                self.shares[filen].clientText = mastertext

    self.in_sync = False
    return False
